﻿//#define INVOKE_TRIGGER_STAY
using Devil;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

namespace GameToolkit
{
    [DisallowMultipleComponent]
    public class RootTriggerCollider : MonoBehaviour
    {
        static readonly string MSG_ENTER = "OnTriggerEnter";
        static readonly string MSG_EXIT = "OnTriggerExit";
#if INVOKE_TRIGGER_STAY
        // 暂不使用，如果必要，后续将开放
        static readonly string MSG_STAY = "OnTriggerStay";
        static readonly List<MonoBehaviour> sMonoCahce = new List<MonoBehaviour>(10);
        static readonly System.Reflection.BindingFlags sBindFlags = 
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
        static readonly System.Type[] sBindParams = new System.Type[] { typeof(Collider) };

        static bool HasTriggerStayMsgReciever(GameObject obj)
        {
            try
            {
                obj.GetComponents(sMonoCahce);
                var baseType = typeof(MonoBehaviour);
                for (int i = 0; i < sMonoCahce.Count; i++)
                {
                    var tp = sMonoCahce[i].GetType();
                    while (tp != null && tp != baseType)
                    {
                        var mtd = tp.GetMethod(MSG_STAY, sBindFlags, null, sBindParams, null);
                        if (mtd != null)
                            return true;
                        tp = tp.BaseType;
                    }
                }
            }
            catch (System.Exception e)
            {
                Debug.LogException(e, obj);
            }
            return false;
        }

        bool mHasStayMsg;
#endif


        ulong mSubTriggersMask;

        internal int RegistSubColliderID(SubTriggerCollider sub)
        {
            if (mSubTriggersMask == 0xffffffffffffffffLU)
            {
                Debug.LogErrorFormat(this, "每个 RootTriggerCollider 最多支持 64 个子触发器，\"{0}\" 无法注册到 \"{1}\"", sub.name, name);
                return -1;
            }
            for (int i = 0; i < 64; i++)
            {
                var flag = 1u << i;
                if ((mSubTriggersMask & flag) == 0)
                {
                    mSubTriggersMask |= flag;
                    return i;
                }
            }
            return -1;
        }

        internal void UnregistSubColliderID(int subid)
        {
            if (subid >= 0 && subid < 64)
            {
                var flag = 1u << subid;
                mSubTriggersMask &= ~flag;
            }
        }

        private void Awake()
        {
#if INVOKE_TRIGGER_STAY
            mHasStayMsg = HasTriggerStayMsgReciever(gameObject);
#endif
        }

        internal void RegistCollider(int subId, Collider other)
        {
            if (subId >= 0 && subId < 64)
            {
                var flag = 1u << subId;
                if ((mSubTriggersMask & flag) != 0)
                    TriggerInvoke.Instance.Regist(this, flag, other);
            }
        }

        internal void UnregistCollider(int subId, Collider other)
        {
            if (subId >= 0 && subId < 64)
            {
                var flag = 1u << subId;
                TriggerInvoke.Instance.Unregist(this, flag, other);
            }
        }

        internal class InvokeTriggerData
        {
            int mId;
            RootTriggerCollider mRoot;
            Collider mOther;
            internal ulong mEnterFlags;
            bool mIsEnter;

            public bool IsVaild { get { return mRoot != null && mOther != null; } }
            public int Id { get { return mId; } }

#if INVOKE_TRIGGER_STAY
            public bool HasStayMsg { get; private set; }
#endif

            public InvokeTriggerData(int id, RootTriggerCollider root, Collider other)
            {
                mId = id;
                mRoot = root;
                mOther = other;
#if INVOKE_TRIGGER_STAY
                HasStayMsg = root.mHasStayMsg;
#endif
            }

            internal bool InvokeTrigger()
            {
                var flag = mEnterFlags & mRoot.mSubTriggersMask;
                if (flag != 0 && !mIsEnter)
                {
                    mIsEnter = true;
                    mRoot.gameObject.SendMessage(MSG_ENTER, mOther, SendMessageOptions.DontRequireReceiver);
                    return true;
                }
                else if (flag == 0 && mIsEnter)
                {
                    mIsEnter = false;
                    mRoot.gameObject.SendMessage(MSG_EXIT, mOther, SendMessageOptions.DontRequireReceiver);
                    return true;
                }
                else
                {
                    return false;
                }
            }

#if INVOKE_TRIGGER_STAY
            internal void InvokeStay()
            {
                mRoot.gameObject.SendMessage(MSG_STAY, mOther, SendMessageOptions.DontRequireReceiver);
            }
#endif

            public override int GetHashCode()
            {
                return mId;
            }
        }

        internal class TriggerInvoke : ICommand
#if INVOKE_TRIGGER_STAY
            , ILoopCommand
#endif
        {

            static TriggerInvoke _inst;
            internal static TriggerInvoke Instance
            {
                get
                {
                    if (_inst == null)
                    {
                        _inst = new TriggerInvoke();

#if INVOKE_TRIGGER_STAY
                        MainThread.StartLooper(_inst);
#endif
                    }
                    return _inst;
                }
            }
            Dictionary<int, InvokeTriggerData> mInvokeDatas;

            HashSet<InvokeTriggerData> mInvokeTriggerMsg;
#if INVOKE_TRIGGER_STAY
            RandomList<InvokeTriggerData> mInvokeStayMsg;
#endif
            int mExecNum;
            readonly int CLEAN_THRESHOLD = 100;

            private TriggerInvoke()
            {
                mInvokeDatas = new Dictionary<int, InvokeTriggerData>();
                mInvokeTriggerMsg = new HashSet<InvokeTriggerData>();
#if INVOKE_TRIGGER_STAY
                mInvokeStayMsg = new RandomList<InvokeTriggerData>(32, (a, b) => a.Id == b.Id);
#endif
            }

            internal void Regist(RootTriggerCollider root, ulong subMask, Collider other)
            {
                var id = MathExt.Hash(root.GetInstanceID(), other.GetInstanceID());
                InvokeTriggerData data;
                if (!mInvokeDatas.TryGetValue(id, out data))
                {
                    data = new InvokeTriggerData(id, root, other);
                    mInvokeDatas.Add(id, data);
                }
                var zero = data.mEnterFlags == 0;
                data.mEnterFlags |= subMask;
                if (zero && mInvokeTriggerMsg.Add(data))
                {
                    MainThread.RunOnMainThread(this, EExecuteEvent.LateUpdate);
                }
            }

            internal void Unregist(RootTriggerCollider root, ulong subMask, Collider other)
            {
                var id = MathExt.Hash(root.GetInstanceID(), other.GetInstanceID());
                InvokeTriggerData data;
                if (mInvokeDatas.TryGetValue(id, out data))
                {
                    data.mEnterFlags &= ~subMask;
                    if (data.mEnterFlags == 0 && mInvokeTriggerMsg.Add(data))
                        MainThread.RunOnMainThread(this, EExecuteEvent.LateUpdate);
                }
            }

            void ICommand.Execute()
            {
#if UNITY_EDITOR
                Profiler.BeginSample("Invoke Trigger Callback");
#endif
                var clean = ++mExecNum >= CLEAN_THRESHOLD;
                if (clean)
                    mExecNum = 0;
                foreach (var invoke in mInvokeTriggerMsg)
                {
                    if (invoke.IsVaild)
                    {
                        var trig = invoke.InvokeTrigger();

#if INVOKE_TRIGGER_STAY
                        if (trig && invoke.HasStayMsg)
                        {
                            if (invoke.mEnterCount > 0)
                                mInvokeStayMsg.Add(invoke);
                            else
                                mInvokeStayMsg.Remove(invoke);
                        }
#endif

                        if (clean && invoke.mEnterFlags == 0)
                        {
                            mInvokeDatas.Remove(invoke.Id);
                        }
                    }
                    else
                    {
                        mInvokeDatas.Remove(invoke.Id);

#if INVOKE_TRIGGER_STAY
                        if (invoke.HasStayMsg)
                            mInvokeStayMsg.Remove(invoke);
#endif
                    }
                }
                mInvokeTriggerMsg.Clear();

#if UNITY_EDITOR
                Profiler.EndSample();
#endif
            }


#if INVOKE_TRIGGER_STAY
            bool ILoopCommand.IsLooping { get { return true; } }
            void ILoopCommand.Execute(float deltaTime)
            {
                for (int i = mInvokeStayMsg.Count - 1; i >= 0; i--)
                {
                    var invoke = mInvokeStayMsg[i];
                    if (invoke.IsVaild)
                    {
                        invoke.InvokeStay();
                    }
                    else
                    {
                        mInvokeStayMsg.RemoveAt(i);
                        mInvokeDatas.Remove(invoke.Id);
                    }
                }
            }
#endif

        }

    }
}
